package edu.upenn.seas.peerreview.smui.variants

import edu.upenn.seas.peerreview.Id
import scala.collection.mutable

/**
 * Accepts decisions from an InputStream and displays game status output to
 * supplied OutputStream. Typically will be Console.in and Console.out
 */
class ConsoleDecider(val in : java.io.BufferedReader, val out : java.io.PrintStream) 
    extends Decider with IdMapper {
  /**
   * The size of the game board.
   */
  private var boardSize : Point = null
  /**
   * Function reference that asks for the state of this player's board.
   */
  private var myBoard : () => Board = null
  /**
   * Function reference that asks for the state of an opponent's board, given
   * an Id.
   */
  private var queryBoard : Id => Board = null
  
  /**
   * Outputs a text notification.
   */
  private def writeln(msg : String) {
    out.println(msg)
  }
  
  /**
   * Outputs the board of an opponent.
   * @param id the id of the opponent to display
   */
  private def printOpponentBoard(id : Id) {
    writeln("Board for " + mapId(id) + ":")
    writeln(queryBoard(id).toString)
  }
  
  /**
   * Display the current game state (your and relevant opponents' boards).
   * @opponents the list of opponents to show
   */
  private def displayAllBoards(opponents : List[Id]) {
    writeln(("=" * 25) + " CURRENT GAME SNAPSHOT " + ("=" * 20))
    opponents.foreach(printOpponentBoard(_))
    writeln("Your board: ")
    writeln(myBoard().toString)
    writeln("=" * 50)
  }
  
  /**
   * Upon registration, save the query functions provided.
   */
  override def onRegister(getMyBoard: () => Board, getOpponentBoard: Id => Board) {
    myBoard = getMyBoard
    queryBoard = getOpponentBoard
    writeln("WELCOME ::: Welcome to Battleship! Waiting for opponents...");
  }
  
  /**
   * When a new game is started, the Application provides the board size and
   * the opponent set.
   */
  override def onNewGame(opponents: List[Id], boardSize: Point) {
    this.boardSize = boardSize
    makeMapping(opponents)
    writeln("A new game has begun! Configurations received.");
  }
  
  /**
   * Ask the user for a sequence of ship placements.
   * @param boardSize the size of the gameboard; should be same as upon registration
   * @param shipSizes the sizes of the ships to place
   * @return a list of tuples, each of which is a ship-start/ship-end pair
   */
  override def placeShips(boardSize: Point, shipSizes: List[Int]) 
     : List[(Point, Point)] = {
    val refBoard = Board.makeBoard(boardSize)
    writeln("\nSHIP PLACING ::: You will now need to place ships of sizes: %s"
        .format(shipSizes.mkString(" ")))
    var quit = false;
    val (placements, boardState) = shipSizes.foldLeft[(List[(Point, Point)], Board)]( 
      (List[(Point, Point)](), refBoard) 
      )( (listBoard,size) => {
        val currentResultList = listBoard._1
        val currentBoardStatus = listBoard._2
        if (quit) listBoard else {
         val result = placeOneShip(currentBoardStatus, size)
         if (result == null) {
           quit = true; 
         }
         if (quit) listBoard 
         else ( (result._1, result._2) :: currentResultList, result._3)
        }
      })
    
    // If quit during the process, recurse and try again. 
    // Else, return ship placements.
    val finalResult = if (quit) placeShips(boardSize, shipSizes) else placements
    writeln("SHIPS PLACED ::: Waiting for your opponents to place their ships...")
    finalResult
  }
  
  /** 
   * Ask the user for a position for this ship.
   * @param board the state of the board with current placements
   * @param size the size of the ship
   * @return (start, end, boardState) upon success, or null if user wants to 
   * restart the placement process
   */
  private[this] def placeOneShip(board: Board, size : Int) : (Point, Point, Board) = {
    /** 
     * Helper to verify ship placement.
     * @return null if invalid, else (start, end, boardState)
     */
    def tryShip(x1 : String, 
                y1 : String, 
                dir : String, 
                vessel : Vessel) : (Point, Point, Board) = {
      var tmpBoard = board
      try {
        val (a, b) = (x1.toInt, y1.toInt)
        val (c, d) = dir match {
          case "v" => (a, b + vessel.size - 1)
          case "h" => (a + vessel.size - 1, b)
          case _ => throw new IllegalArgumentException()
        }
        val placesOkay = for (i <- (a to c); j <- (b to d)) yield {
          val place = Point(i, j)
          if (tmpBoard.examine(place) != CellStatus.Empty) 0 else {
            tmpBoard = tmpBoard + (place, CellStatus.Ship(vessel) )
            1
          }
        }
        val intermediateList = placesOkay.toList;
        val total = intermediateList.foldLeft(0)(_ + _)
        if (total == size) return (Point(a, b), Point(c, d), tmpBoard) else {
          writeln("ERROR: Your position overlaps or does not fit on the board.")
          return null
        }
      } catch {
        case e : NumberFormatException => { 
          writeln("ERROR: Invalid number format. Try again."); 
          return null 
        }
        case e : IllegalArgumentException => {
          writeln("ERROR: Must specify (h)orizontal or (v)ertical placement.")
          return null
        }
      }
    } 
    
    val ship = Vessel(size)
    writeln("=" * 80)
    writeln(" \nYour current board:")
    writeln(board.toString)
    writeln("**** Please choose a place for your %s of size %d."
        .format(ship.name, ship.size))
    writeln("Enter in form: x y (h/v). For example: 0 0 h. Press q to start over all ships.")
    writeln("(x, y) is the ship top/left and extends horizontally/vertically.")
    val input : List[String]= in.readLine().split(" ").toList.remove(_.trim == "")
    input match {
      case "q" :: Nil => { writeln("\n>>> Starting over!\n"); null }
      case a :: b :: dir :: xs if dir == "h" || dir == "v" => {
        val shipPlace = tryShip(a, b, dir, ship)
        if (shipPlace != null) return shipPlace else placeOneShip(board, size)
       }
      case _ => { writeln("Invalid format. Try again!"); placeOneShip(board, size)}
    }
  }
  
  
  /**
   * Verify that a move is in the legal range and to a valid opponent.
   * @return (Id, Point) if valid, else null
   */
  private[this] def validateMove(opps : List[Id], 
                   oppToString : String, 
                   move1 : String, 
                   move2 : String) : (Id, Point) = {
      try {
        val x = move1.toInt
        val y = move2.toInt
        if (!Board.makeBoard(boardSize).inRange(Point(x, y))) return null
        val matchingOpponent : List[Id] = opps.filter(mapId(_) == oppToString)
        if (matchingOpponent.isEmpty) return null
        return (matchingOpponent.head, Point(x, y))
      } catch {
        case e : NumberFormatException => return null
      }
      return null
  }
  
  /**
   * Ask for the player to make a move.
   * @param opps the list of active opponents to aim at
   */
  override def makeMove(opps : List[Id] ) : (Id, Point) = { 
    displayAllBoards(opps)
    writeln("*** Please make a move: type [opponent x y]. For example: SM7 2 12")
    return getMoveFromUser(opps, boardSize);
  }
  
  /**
   * Implements the logic that fetches a move from the inputstream.
   * @VisibleForTesting
   */
  private[variants] def getMoveFromUser(opps: List[Id], boardSize: Point) : (Id, Point) = {
    val err = "Invalid - please try again!"
    while (true) {
      val input : List[String]= in.readLine().split(" ").toList.remove(_.trim == "")
      input match {
        case id :: move1 :: move2 :: xs => {
          val trialMove = validateMove(opps, id, move1, move2)
          if (trialMove != null) return trialMove 
          else writeln(err)
        }
        case _ => writeln(err)
      }
    }
    throw new RuntimeException("Should not have returned here...")
 }

  
  override def onOpponentMoved(opp:Id) { 
    writeln("\t%s has submitted his/her move.".format(mapId(opp)))
  }

  override def onBeingFiredAt(from: Id, loc:Point) { 
    writeln("\t%s fired at your point %s!".format(mapId(from), loc))
  } 
  
  override def onFiredAtReport(oppFrom:Id, loc:Point, result:FireResultType) { 
    writeln("BOOM! %s fired at YOU at %s with result: %s"
        .format(mapId(oppFrom), loc, result))  
  }
  
  override def onOpponentFireReport(oppFrom:Id, target:Id, loc:Point, result:FireResultType) { 
    writeln("INFO: %s fired at %s, at %s with result: %s"
        .format(mapId(oppFrom), mapId(target),  loc, result))  
  }
  
  override def onYourFireReport(target:Id, loc:Point, result:FireResultType) { 
    writeln("RESULT: You fired at %s, at %s with result: %s"
        .format(mapId(target), loc, result))  
  }

  override def onPlayerResult(opp:Id, hits:Int, sinks:Int, kills:Int, hasWon:Boolean) { 
    writeln("\n>>> Player out of game: %s\n".format(mapId(opp)))
  }  
  
  override def onPlayerWithdrawal(opp:Id) { 
    writeln("Player quit the gmae: " + mapId(opp))
  }
  
  override def onGameOver() {
    writeln("Game over!")
  }
}
